#include <vector>
#include <map>
#include <string>
#include <SymbolTable.h>
#include <netthumb.h>
#include <driver.h>
#include <Context.h>
#include <iostream>
#include <netthumbVisitor.h>
#include <stdlib.h>
#include <sstream>

namespace netthumb {

  ContextI::ContextI(SymbolTable& sTable):
    _symbolTable(sTable)
  {
    _attrMap.clear();
  }

  void
  ContextI::setDriver(Driver* drv)
  {
    _driver = drv;
  }

  void
  ContextI::insertAttribute(int key, string val)
  {
    _attrMap[key] = val;
  }

  SymbolTable*
  ContextI::getSymbolTablePtr()
  {
    return &_symbolTable;
  }

  void
  ContextI::visit(SymbolTable* stable)
  {
    stable->_valid = checkNetInDevices(stable) && checkNetInRoutes(stable);
  }

  bool
  ContextI::checkNetInRoutes(SymbolTable* stable)
  {
    /*
      Comprueba que todas las redes presentes
      en las tablas de rutas existan.
    */

    bool ok = true;

    map<string, NetworkI*> networks = stable->getNetworks();

    // Comprobacion de los hosts.
    map<string, HostI*>::iterator hostIt;
    for(hostIt = stable->getHosts().begin();
	hostIt != stable->getHosts().end();
	hostIt++) {

      vector<ConfRouteI*>::iterator hconfIt;
      for(hconfIt = hostIt->second->getRoute()->getConfigurations().begin();
	  hconfIt != hostIt->second->getRoute()->getConfigurations().end();
	  hconfIt++) {

	if(networks.count((*hconfIt)->getNetID()) == 0){
	  // Error: Network no declarada.
	  ostringstream errOut;
	  errOut << "Error in Host \'" << hostIt->first << "\' (";
	  errOut << (*hconfIt)->getLocation() << "): Network \'" << (*hconfIt)->getNetID();
	  errOut << "\' not declared";
	  _driver->error(errOut.str());	  
	  ok = false;
	}
      }
    }

    // Comprobacion de los routers.
    map<string, RouterI*>::iterator routerIt;
    for(routerIt = stable->getRouters().begin();
	routerIt != stable->getRouters().end();
	routerIt++) {

      vector<ConfRouteI*>::iterator hconfIt;
      for(hconfIt = routerIt->second->getRoute()->getConfigurations().begin();
	  hconfIt != routerIt->second->getRoute()->getConfigurations().end();
	  hconfIt++) {

	if(networks.count((*hconfIt)->getNetID()) == 0){
	  // Error: Network no declarada.
	  ostringstream errOut;
	  errOut << "Error in Router \'" << routerIt->first << "\' (";
	  errOut << (*hconfIt)->getLocation() << "): Network \'" << (*hconfIt)->getNetID();
	  errOut << "\' not declared";
	  _driver->error(errOut.str());	  
	  ok = false;
	}
      }
    }
    return ok;
  }

  bool 
  ContextI::checkNetInDevices(SymbolTable* stable)
  {
    /* 
       Comprueba que todos los devices esten
       conectados a redes que existan.
    */

    bool ok = true;

    map<string, NetworkI*> networks = stable->getNetworks();

    // Comprobacion de los hosts.
    map<string, HostI*>::iterator hostIt;
    for(hostIt = stable->getHosts().begin();
	hostIt != stable->getHosts().end();
	hostIt++) {
      map<string, DeviceI*>::iterator devsIt; 
      for( devsIt = hostIt->second->getDevices().begin();
	   devsIt != hostIt->second->getDevices().end();
	   devsIt++){
	if(networks.count(devsIt->second->getNetID()) == 0){
	  // Error: Network no declarada.
	  ostringstream errOut;
	  errOut << "Error in Host \'" << hostIt->first << "\' (";
	  errOut << devsIt->second->getLocation() << "): Network \'" << devsIt->second->getNetID();
	  errOut << "\' in Device \'" << devsIt->first << "\' not declared";
	  _driver->error(errOut.str());	  
	  ok = false;
	}
      }
    }

    // Comprobacion de los routers.
    map<string, RouterI*>::iterator routerIt;
    for(routerIt = stable->getRouters().begin();
	routerIt != stable->getRouters().end();
	routerIt++) {
      // Comprobacion de los hosts.
      map<string, DeviceI*>::iterator devsIt; 
      for( devsIt = routerIt->second->getDevices().begin();
	   devsIt != routerIt->second->getDevices().end();
	   devsIt++){
	if(networks.count(devsIt->second->getNetID()) == 0){
	  // Error: Network no declarada.
	  ostringstream errOut;
	  errOut << "Error in Router \'" << routerIt->first <<"\' (";
	  errOut << routerIt->second->getLocation() << "): Network \'";
	  errOut << devsIt->second->getNetID() << "\' in Device \'" << devsIt->first << "\' not declared";
	  _driver->error(errOut.str());	  
	  ok = false;
	}
      }
    }
    return ok;
  }
   
  void
  ContextI::visit(DeviceI* device)
  {

    if(_deviceMap.count(_attrMap[DEVICE_ID])){
      ostringstream errMsg;
      errMsg << "Warning: Device \'" << _attrMap[DEVICE_ID] << "\' declared in ";
      errMsg << _attrMap[DEVICE_LOCATION] << " was previously declared in ";
      errMsg << _deviceMap[_attrMap[DEVICE_ID]]->getLocation() << "; skipping";
      _driver->error(errMsg.str());
      return;
    }

    map<string, LinkSt> linkmap;
    map<string, NetScope> scopemap;

    linkmap[string("nullSt")] = nullSt;
    linkmap[string("up")] = up;
    linkmap[string("down")] = down;

    scopemap[string("null")] = nullScope;
    scopemap[string("global")] = global;
    scopemap[string("site")] = site;
    scopemap[string("link")] = link;
    scopemap[string("host")] = host;

    device->populate(_attrMap[DEVICE_ADDRID],
		     _attrMap[DEVICE_NETID],
		     _attrMap[DEVICE_LINKID],
		     _attrMap[DEVICE_LOCATION]);

    if(_attrMap.count(DEVICE_LINKST))
      device->setLinkSt(linkmap[_attrMap[DEVICE_LINKST]]);

    if(_attrMap.count(DEVICE_NETSCOPE))
      device->setScope(scopemap[_attrMap[DEVICE_NETSCOPE]]);

    // Almacena la instancia en la lista de Devices.
    _deviceMap[_attrMap[DEVICE_ID]] = device;

    // Limpia la tabla de atributos
    _attrMap.clear();
  }

  void
  ContextI::visit(ConfRouteI* conf)
  {
    map<string, NetType> typemap;

    typemap[string("null")] = nullType;
    typemap[string("unicast")] = unicast;
    typemap[string("unreachable")] = unreachable;
    typemap[string("blackhole")] = blackhole;
    typemap[string("prohibit")] = prohibit;
    typemap[string("local")] = local;
    typemap[string("broadcast")] = broadcast;
    typemap[string("thrown")] = thrown;
    typemap[string("nat")] = nat;
    typemap[string("anycast")] = anycast;
    typemap[string("multicast")] = multicast;

    conf->populate(_attrMap[CONFROUTE_NETID],
		   _attrMap[CONFROUTE_DEVICEID],
		   _attrMap[CONFROUTE_LOCATION]);

    if(_attrMap.count(CONFROUTE_NETTYPE))
      conf->setNetType(typemap[_attrMap[CONFROUTE_NETTYPE]]);
    
    if(_attrMap.count(CONFROUTE_VIAADDR)){
      conf->setViaAddr(_attrMap[CONFROUTE_VIAADDR]);
    }

    if(_attrMap[CONFROUTE_MORP] == string("metric"))
      // Opcion metric
      conf->setMetricOrPriority(true);
    else
      // Opcion priority
      conf->setMetricOrPriority(false);

    if (_attrMap.count(CONFROUTE_METRIC)){
      string metricval = _attrMap[CONFROUTE_METRIC];
      conf->setMetric(atoi(metricval.c_str()));
    }

    // Inserta los parametros de configuracion en la lista.
    _confRouteVector.push_back(conf);
    
    // Limpia el mapa de atributos.
    _attrMap.clear();
  }

  void
  ContextI::visit(RouteI* route)
  {

    route->setLocation(_attrMap[ROUTE_LOCATION]);
    route->setDeviceID(_attrMap[ROUTE_DEVICEID]);

    if(_attrMap.count(ROUTE_VIAADDR))
      route->setViaAddr(_attrMap[ROUTE_VIAADDR]);

    if(_attrMap[ROUTE_MORP] == string("metric"))
      // Opcion metric
      route->setMetricOrPriority(true);
    else
      // Opcion priority
      route->setMetricOrPriority(false);
   
     if (_attrMap.count(ROUTE_METRIC)){
       string metricval = _attrMap[ROUTE_METRIC];
       route->setMetric(atoi(metricval.c_str()));
     }

    // Almacena todas las configuraciones previamente generadas.
    vector<ConfRouteI*>::iterator it;
    for(it = _confRouteVector.begin(); it != _confRouteVector.end(); it++)
      route->insertConf(*it);

    _route = route; // Guarda la informacion de ruta actual.

    // Limpia el vector de configuraciones.
    _confRouteVector.clear();

    // Limpia el mapa de atributos.
    _attrMap.clear();
  }

  void
  ContextI::visit(HostI* host)
  {

    HostI allocatedHost;

    if(_symbolTable.getHostByID(_attrMap[HOST_ID], allocatedHost)){
      ostringstream errMsg;
      errMsg << "Warning: Host \'" << _attrMap[HOST_ID];
      errMsg << "\' declared in " << _attrMap[HOST_LOCATION] << " was previously declared in ";
      errMsg << allocatedHost.getLocation() << "; skipping";
      _driver->error(errMsg.str());
      return;
    }

    host->setLocation(_attrMap[HOST_LOCATION]);
    host->setDevices(_deviceMap);
    host->setRoute(_route);

    // Inserta el host en la tabla de simbolos.
    _symbolTable.insertNewHost(host, _attrMap[HOST_ID]);

    // Limpia la tabla de atributos.
    _attrMap.clear();

    // Limpia la tabla de devices.
    _deviceMap.clear();
  }

  void
  ContextI::visit(RouterI* router)
  {

    RouterI allocatedRouter;

    if(_symbolTable.getRouterByID(_attrMap[ROUTER_ID], allocatedRouter)){
      ostringstream errMsg;
      errMsg << "Warning: Router \'" << _attrMap[ROUTER_ID] << "\' declared in ";
      errMsg << _attrMap[ROUTER_LOCATION] << " was previously declared in ";
      errMsg << allocatedRouter.getLocation() << "; skipping";
      _driver->error(errMsg.str());
      return;
    }

    router->setLocation(_attrMap[ROUTER_LOCATION]);
    router->setDevices(_deviceMap);
    router->setRoute(_route);

    // Inserta el router en la tabla de simbolos.
    _symbolTable.insertNewRouter(router, _attrMap[ROUTER_ID]);

    // Limpia la tabla de atributos.
    _attrMap.clear();

    // Limpia la tabla de devices.
    _deviceMap.clear();
  }

  void
  ContextI::visit(NetworkI* network)
  {

    NetworkI allocatedNetwork;

    if(_symbolTable.getNetworkByID(_attrMap[NETWORK_ID], allocatedNetwork)){
      ostringstream errMsg;
      errMsg <<"Warning: Network \'" << _attrMap[NETWORK_ID] << "\' declared in ";
      errMsg << _attrMap[NETWORK_LOCATION] << " was previously declared in " << allocatedNetwork.getLocation() << "; skipping";
      _driver->error(errMsg.str());
      return;
    }

    int mtu = atoi(_attrMap[NETWORK_MTU].c_str());
    bool multicast = _attrMap[NETWORK_MULTICAST] == string("on") ? true : false;
    
    network->populate(_attrMap[NETWORK_ADDRID],
		      _attrMap[NETWORK_TYPEID],
		      mtu,
		      multicast,
		      _attrMap[NETWORK_LOCATION]);

    // Inserta la informacion de la red en la tabla de simbolos
    _symbolTable.insertNewNetwork(network, _attrMap[NETWORK_ID]);

    // Limpia la tabla de atributos.
    _attrMap.clear();
  }
}
